// 《围棋》作者版权所有。保留所有权利。
// 此源代码的使用受BSD样式
// 许可证的约束，该许可证可以在许可证文件中找到。

// Package lockedfile创建和操作文件，其内容只应原子地更改。
package lockedfile

import (
	"fmt"
	"io"
	"io/fs"
	"os"
	"runtime"
)

// 文件是一个锁定的*操作系统。文件
// 
// 关闭文件会释放锁。
// 
// 如果程序在文件被锁定时退出，操作系统会释放
// 锁，但可能不会立即释放：调用者必须确保在退出前关闭所有锁定的
// 文件。
type File struct {
	osFile
	closed bool
}

// osFile嵌入了一个*os。文件，同时保持指针本身未报告。
// （当我们关闭一个文件时，它必须是我们打开的同一个文件描述符！）
type osFile struct {
	*os.File
}

// OpenFile就像操作系统。OpenFile，但返回一个锁定的文件。
// 如果标志包括操作系统。O_WRONLY或os。O_RDWR，文件被写锁定；
// 否则，它将被读锁定。
func OpenFile(name string, flag int, perm fs.FileMode) (*File, error) {
	var (
		f   = new(File)
		err error
	)
	f.osFile.File, err = openFile(name, flag, perm)
	if err != nil {
		return nil, err
	}

	// 虽然当go 
	// 命令退出时，操作系统会对打开的文件解除锁定，但我们希望尽可能短的时间保持锁定，尤其是我们不希望在处理完文件后将其锁定。我们的
	// Close方法是释放锁的方法，因此请使用终结器尽最大努力报告
	// 缺少关闭调用。
	runtime.SetFinalizer(f, func(f *File) {
		panic(fmt.Sprintf("lockedfile.File %s became unreachable without a call to Close", f.Name()))
	})

	return f, nil
}

// 开放就像操作系统。打开，但返回一个读锁定文件。
func Open(name string) (*File, error) {
	return OpenFile(name, os.O_RDONLY, 0)
}

// 创建就像操作系统。创建，但返回一个写锁定文件。
func Create(name string) (*File, error) {
	return OpenFile(name, os.O_RDWR|os.O_CREATE|os.O_TRUNC, 0666)
}

// Edit使用模式0666（在umask之前）创建命名文件，但不截断现有内容。
// 
// 如果编辑成功，返回文件上的方法可用于I/O。
// 关联的文件描述符具有模式O_RDWR，文件被写锁定。
func Edit(name string) (*File, error) {
	return OpenFile(name, os.O_RDWR|os.O_CREATE, 0666)
}

// Close解锁并关闭基础文件。
// 
// Close可以多次调用；第一次之后的所有调用将返回
// 非零错误。
func (f *File) Close() error {
	if f.closed {
		return &fs.PathError{
			Op:   "close",
			Path: f.Name(),
			Err:  fs.ErrClosed,
		}
	}
	f.closed = true

	err := closeFile(f.osFile.File)
	runtime.SetFinalizer(f, nil)
	return err
}

// Read使用读锁打开命名文件并返回其内容。
func Read(name string) ([]byte, error) {
	f, err := Open(name)
	if err != nil {
		return nil, err
	}
	defer f.Close()

	return io.ReadAll(f)
}

// Write打开指定的文件（如果需要，使用给定的权限创建），
// 然后Write将其锁定并用给定的内容覆盖。
func Write(name string, content io.Reader, perm fs.FileMode) (err error) {
	f, err := OpenFile(name, os.O_WRONLY|os.O_CREATE|os.O_TRUNC, perm)
	if err != nil {
		return err
	}

	_, err = io.Copy(f, content)
	if closeErr := f.Close(); err == nil {
		err = closeErr
	}
	return err
}

// Transform调用t，读取指定文件的结果，其锁
// 仍然保持。
// 
// 如果t返回零错误，则Transform会将返回的内容写回文件
// 尽最大努力保留错误时的现有内容。
// 
// t不能修改传递给它的切片。
func Transform(name string, t func([]byte) ([]byte, error)) (err error) {
	f, err := Edit(name)
	if err != nil {
		return err
	}
	defer f.Close()

	old, err := io.ReadAll(f)
	if err != nil {
		return err
	}

	new, err := t(old)
	if err != nil {
		return err
	}

	if len(new) > len(old) {
		// 总体文件大小正在增加，所以先写下尾部：如果我们的
		// 磁盘空间即将耗尽，我们宁愿在覆盖原始内容之前检测到
		// 失败。
		if _, err := f.WriteAt(new[len(old):], int64(len(old))); err != nil {
			// 尽最大努力去除不完整的尾巴。
			f.Truncate(int64(len(old)))
			return err
		}
	}

	// 我们将覆盖旧内容。如果失败，请在关闭文件之前尽最大努力回滚。
	defer func() {
		if err != nil {
			if _, err := f.WriteAt(old, 0); err == nil {
				f.Truncate(int64(len(old)))
			}
		}
	}()

	if len(new) >= len(old) {
		if _, err := f.WriteAt(new[:len(old)], 0); err != nil {
			return err
		}
	} else {
		if _, err := f.WriteAt(new, 0); err != nil {
			return err
		}
		// 文件的总大小正在减小，因此在写入后将文件缩小到其最终大小
		// 。我们在写入后（而不是之前）执行此操作，这样，如果写入失败，可能仍会保留足够的文件系统空间来包含以前的内容。
		if err := f.Truncate(int64(len(new))); err != nil {
			return err
		}
	}

	return nil
}
